package com.chouren.goandroid.http.client;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.Map.Entry;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.ProtocolException;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.RedirectHandler;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HttpContext;

import android.content.Context;
import android.os.Build;
import android.util.Log;

import com.chouren.goandroid.http.comm.Charsets;
import com.chouren.goandroid.http.comm.KeyValuePair;
import com.chouren.goandroid.http.exception.HttpClientException;
import com.chouren.goandroid.http.exception.HttpNetException;
import com.chouren.goandroid.http.exception.HttpServerException;
import com.chouren.goandroid.http.exception.HttpServerException.ServerException;
import com.chouren.goandroid.http.parser.DataParser;
import com.chouren.goandroid.http.request.HttpEntityBuilder;
import com.chouren.goandroid.http.request.Request;
import com.chouren.goandroid.http.request.Request.Abortable;
import com.chouren.goandroid.http.request.param.HttpParam;
import com.chouren.goandroid.http.response.HttpStatus;
import com.chouren.goandroid.http.response.InternalResponse;
import com.chouren.goandroid.http.response.Response;

/**
 * 对ApacheHttpClient的封装
 * @version 1.0 
 * @author Administrator
 * @date 2014年7月2日
 * @update 2014年7月2日
 */
public class ApacheHttpClient extends HttpClient {
	private static String TAG = ApacheHttpClient.class.getSimpleName();
	
	public static final int DEFAULT_KEEP_LIVE = 30000;
	public static final int DEFAULT_MAX_CONN_PER_ROUT = 128;
	public static final int DEFAULT_MAX_CONN_TOTAL = 512;
	public static final boolean TCP_NO_DELAY = true;
	
	private static ApacheHttpClient instance;
	private DefaultHttpClient mHttpClient;

	private ApacheHttpClient(int retrySleep, boolean forceRetry) {
		mHttpClient = createApacheHttpClient(createHttpParams());
	}
	
	public synchronized static ApacheHttpClient getInstance(Context context, int retrySleep, 
			boolean forceRetry) {
		if (instance == null) {
			if (context != null) appContext = context.getApplicationContext();
			instance = new ApacheHttpClient(retrySleep, forceRetry);
		}
		return instance;
	}
	
	@Override
	public synchronized Response execute(Request request) {
		final InternalResponse response = getInternalResponse();
		if(request == null) return response;
		if(response != null) {
			response.setRequest(request);
			response.setDataParser(request.getDataParser());
		}
		
		try {
			readDataWithRetries(request, response);
		} catch (HttpClientException e) {
			e.printStackTrace();
		} catch (HttpServerException e) {
			e.printStackTrace();
		} catch (HttpNetException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		return response;
	}
	
	public HttpResponse execute(HttpUriRequest req) throws ClientProtocolException, IOException {
		return mHttpClient.execute(req);
	}

	public <T> T execute(String uri, DataParser<T> parser, HttpMethod method) {
		execute(new Request(uri, parser).setMethod(method));
		return parser.getData();
	}

	@Override
	public String get(String uri) {
		return null;
	}

	@Override
	public <T> T get(String uri, DataParser<T> parser) {
		return null;
	}

	@Override
	public <T> T get(String uri, HttpParam model, Class<T> claxx) {
		return null;
	}

	@Override
	public String post(String uri) {
		return null;
	}

	@Override
	public <T> T post(String uri, DataParser<T> parser) {
		return null;
	}

	@Override
	public <T> T post(String uri, HttpParam model, Class<T> claxx) {
		return null;
	}
	
	/**
	 * initialize HttpParams , initialize settings such as total
	 * connextions,timeout ...
	 * @return BasicHttpParams
	 */
	private BasicHttpParams createHttpParams() {
		BasicHttpParams params = new BasicHttpParams();
		ConnManagerParams.setTimeout(params, HttpClientConfig.DEFAULT_TIMEOUT);
		ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(DEFAULT_MAX_CONN_PER_ROUT));
		ConnManagerParams.setMaxTotalConnections(params, DEFAULT_MAX_CONN_TOTAL);
		HttpConnectionParams.setTcpNoDelay(params, TCP_NO_DELAY);
		HttpConnectionParams.setConnectionTimeout(params, HttpClientConfig.DEFAULT_TIMEOUT);
		HttpConnectionParams.setSoTimeout(params, HttpClientConfig.DEFAULT_TIMEOUT);
		HttpConnectionParams.setSocketBufferSize(params, HttpClientConfig.DEFAULT_BUFFER_SIZE);
		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
		HttpProtocolParams.setUserAgent(params, String.format("GoAndroid %s ( http://www.google.com )", "1.0"));
		return params;
	}
	
	/**
	 * new {@link DefaultHttpClient}, and set strategy.
	 * @param httpParams
	 * @return DefaultHttpClient
	 */
	private DefaultHttpClient createApacheHttpClient(BasicHttpParams httpParams) {
		DefaultHttpClient httpClient = new DefaultHttpClient(createClientConnManager(httpParams), httpParams);
		// disable apache default redirect handler
		httpClient.setRedirectHandler(new RedirectHandler() {

			@Override
			public boolean isRedirectRequested(HttpResponse response, HttpContext context) {
				return false;
			}

			@Override
			public URI getLocationURI(HttpResponse response, HttpContext context) throws ProtocolException {
				return null;
			}
		});
		// disable apache default retry handler
		httpClient.setHttpRequestRetryHandler(new HttpRequestRetryHandler() {
			@Override
			public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
				return false;
			}
		});
		// enable gzip supporting in request
		httpClient.addRequestInterceptor(new HttpRequestInterceptor() {
			@Override
			public void process(org.apache.http.HttpRequest request, HttpContext context) {
				if (!request.containsHeader(HttpClientConfig.HEADER_ACCEPT_ENCODING)) {
					request.addHeader(HttpClientConfig.HEADER_ACCEPT_ENCODING, HttpClientConfig.ENCODING_GZIP);
				}
			}
		});
		// enable gzip supporting in response
		httpClient.addResponseInterceptor(new HttpResponseInterceptor() {
			@Override
			public void process(HttpResponse response, HttpContext context) {
				final HttpEntity entity = response.getEntity();
				if (entity == null) { return; }
				final Header encoding = entity.getContentEncoding();
				if (encoding != null) {
					for (HeaderElement element : encoding.getElements()) {
						if (element.getName().equalsIgnoreCase(HttpClientConfig.ENCODING_GZIP)) {
							response.setEntity(new GZIPEntityWrapper(entity));
							break;
						}
					}
				}
			}
		});
		// setKeepAlive(httpClient);
		return httpClient;
	}
	
	/**
	 * register http and https scheme, and got ThreadSafeClientConnManager
	 * @param httpParams
	 * @return ThreadSafeClientConnManager
	 */
	private ThreadSafeClientConnManager createClientConnManager(BasicHttpParams httpParams) {
		SchemeRegistry schemeRegistry = new SchemeRegistry();
		// Android had a bug where HTTPS made reverse DNS lookups (fixed in Ice Cream Sandwich)
		// http://code.google.com/p/android/issues/detail?id=13117
		SocketFactory socketFactory = null;
		if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
			socketFactory = new MyLayeredSocketFactory();
		} else {
			socketFactory = SSLSocketFactory.getSocketFactory();
		}
		schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 
				HttpClientConfig.DEFAULT_HTTP_PORT));
		schemeRegistry.register(new Scheme("https", socketFactory, HttpClientConfig.DEFAULT_HTTPS_PORT));
		return new ThreadSafeClientConnManager(httpParams, schemeRegistry);
	}
	
	/**
	 * 连接网络读取数据
	 * @param request
	 * @param innerResponse
	 * @throws HttpNetException
	 * @throws HttpClientException
	 * @throws IOException 
	 * @throws ClientProtocolException 
	 * @throws URISyntaxException 
	 * @throws HttpServerException
	 * @throws InterruptedException
	 */
	private void readDataWithRetries(Request request, InternalResponse innerResponse) 
			throws HttpClientException, HttpServerException, HttpNetException, InterruptedException {
		final HttpUriRequest httpRequest = createApacheHttpRequest(request);
		
		// add headers
		if(request.getHeaders() != null) {
			for(Entry<String, String> entry : request.getHeaders().entrySet()) {
				httpRequest.addHeader(new BasicHeader(entry.getKey(), entry.getValue()));
			}
		}
		
		// set http can abort
		request.setAbort(new Abortable() {
			@Override
			public void abort() {
				httpRequest.abort();
			}
		});
		
		// try connect 
		HttpResponse response = null;
		IOException cause = null;
		
		try {
			if (!Thread.currentThread().isInterrupted()) {
				// connect
				if (doStatistics) innerResponse.getExecuteListener().onPreConnect();
				response = mHttpClient.execute(httpRequest);
				if (doStatistics) innerResponse.getExecuteListener().onAfterConnect();
				
				// status
				StatusLine status = response.getStatusLine();
				HttpStatus httpStatus = new HttpStatus(status.getStatusCode(), status.getReasonPhrase());
				innerResponse.setHttpStatus(httpStatus);
				
				// headers 
				Header[] headers = response.getAllHeaders();
				if(headers != null) {
					KeyValuePair kvp[] = new KeyValuePair[headers.length];
					for(int i = 0; i < headers.length; i++) {
						String name = headers[i].getName();
						String value = headers[i].getValue();
						if ("Content-Length".equals(name)) {
							innerResponse.setContentLength(Long.parseLong(value));
						}
						kvp[i] = new KeyValuePair(name, value);
					}
					innerResponse.setHeaders(kvp);
				}
				
				// body
				if(status.getStatusCode() <= 299 || status.getStatusCode() == 600) {
					if(useDebug) Log.w(TAG, "status:" + status.getStatusCode());
					HttpEntity entity = response.getEntity();
					if(entity != null) {
						// charset
						String charSet = getCharsetFromEntity(entity, request.getCharSet());
						innerResponse.setCharSet(charSet);
						// length
						long len = innerResponse.getContentLength();
						DataParser<?> parser = innerResponse.getDataParser();
						if (!Thread.currentThread().isInterrupted()) {
							if (doStatistics) innerResponse.getExecuteListener().onPreRead();
							parser.readInputStream(entity.getContent(), (int) len, charSet);
							if (doStatistics) innerResponse.getExecuteListener().onAfterRead();
							innerResponse.setReadedLength(parser.getReadLength());
						} else {
							Log.w(TAG, "DataParser readInputStream :currentThread isInterrupted ");
						}
					}
				} else if(status.getStatusCode() <= 399) {
					// redirect
					if (innerResponse.getRedirectTimes() < HttpClientConfig.DEFAULT_MAX_REDIRECT_TIMES) {
						// get the location header to find out where to redirect to
						Header locationHeader = response.getFirstHeader(HttpClientConfig.REDIRECT_LOCATION);
						if (locationHeader != null) {
							String location = locationHeader.getValue();
							if (location != null) {
								if (!location.toLowerCase(Locale.US).startsWith("http")) {
									URI uri = new URI(request.getUrl());
									URI redirect = new URI("http", uri.getHost(), location, null);
									location = redirect.toString();
								}
								innerResponse.setRedirectTimes(innerResponse.getRedirectTimes() + 1);
								request.setUrl(location);
								Log.i(TAG, "Redirect to : " + location);
								readDataWithRetries(request, innerResponse);
								return;
							}
						}
						throw new HttpServerException(httpStatus);
					} else {
						throw new HttpServerException(ServerException.RedirectTooMany);
					} 
				} else if (status.getStatusCode() <= 499) {
					// 客户端被拒
					throw new HttpServerException(httpStatus);
				} else if (status.getStatusCode() < 599) {
					// 服务器有误
					throw new HttpServerException(httpStatus);
				}
			}else {
				Log.w(TAG, "While read :currentThread isInterrupted ");
			}
		} catch (NumberFormatException e) {
			throw new HttpNetException(cause);
		} catch (URISyntaxException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} 
	}
	
	/**
	 * 获取apache的http request对象
	 * @return
	 * @throws HttpClientException 
	 */
	private HttpUriRequest createApacheHttpRequest(Request request) throws HttpClientException {
		String rawUri = request.getUrl();
		HttpEntityEnclosingRequestBase entityRequset = null;
		switch(request.getMethod()) {
		case Get :
			return new HttpGet(request.getUrl());
		case Head :
			return new HttpHead(request.getUrl());
		case Delete :
			return new HttpDelete(request.getUrl());
		case Trace :
			return new HttpTrace(request.getUrl());
		case Options :
			return new HttpOptions(request.getUrl());
		case Post :
			entityRequset = new HttpPost(rawUri);
			break;
		case Put :
			entityRequset = new HttpPut(rawUri);
			break;
		default :
			return new HttpGet(request.getUrl());
		}
		
		if (entityRequset != null) {
			entityRequset.setEntity(HttpEntityBuilder.build(request));
		}
		return entityRequset;
	}
	
	/**
	 * get Charset String From HTTP Response
	 * @param entity
	 * @param defCharset
	 * @return
	 */
	private String getCharsetFromEntity(HttpEntity entity, String defCharset) {
		final Header header = entity.getContentType();
		if (header != null) {
			final HeaderElement[] elements = header.getElements();
			if (elements.length > 0) {
				HeaderElement helem = elements[0];
				final NameValuePair[] params = helem.getParameters();
				if (params != null) {
					for (final NameValuePair param : params) {
						if (param.getName().equalsIgnoreCase("charset")) {
							String s = param.getValue();
							if (s != null) { return s; }
						}
					}
				}
			}
		}
		return defCharset == null ? Charsets.UTF_8 : defCharset;
	}

}
